﻿using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Collections;
using System.Reflection;

namespace Sker.Controls
{
    /// <summary>
    /// Represents a control that can be used to filter items in a collection of data with auto-complete capabilities.
    /// </summary>
    public partial class SearchBox : UserControl
    {
        #region PROPERTIES/FIELDS
        /// <summary>
        /// Overrides the default search method. 
        /// It can be used, for example, to connect the SearchBox to a web service instead of searching in a fixed list.
        /// </summary>
        public Func<string, IEnumerable> SearchFunction { get; set; }

        /// <summary>Identifies the SelectedItem dependency property.</summary>
        public static DependencyProperty SelectedItemProperty;

        /// <summary>Item selected during a search. This is a dependency property.</summary>
        public object SelectedItem
        {
            get { return GetValue(SelectedItemProperty); }
            set { SetValue(SelectedItemProperty, value); }
        }

        /// <summary>Field which holds the DefaultText property.</summary>
        private string defaultText;

        /// <summary>Text displayed in the SearchBox when it's empty.</summary>
        public string DefaultText
        {
            get { return defaultText; }
            set { searchBox.Text = defaultText = value; }
        }

        /// <summary>Text displayed at the bottom of the results list when no results were found.</summary>
        public string NoResultsText 
        {
            get { return noResults.Content.ToString(); }
            set { noResults.Content = value; }
        }

        /// <summary>The SearchBox background. An ImageBrush can be used in order to change the icon.</summary>
        public new Brush Background
        {
            get { return searchBox.Background; }
            set { searchBox.Background = value; }
        }

        /// <summary>Identifies the ItemsSource dependency property.</summary>
        public static DependencyProperty ItemsSourceProperty;

        /// <summary>Gets or sets the collection used to search. This is a dependency property.</summary>
        public IEnumerable ItemsSource
        {
            get { return (IEnumerable)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        /// <summary>Gets or sets a path to a value on the source object to serve as the visual representation of the object, otherwise ToString() is used.</summary>
        public string DisplayMemberPath
        {
            get { return searchList.DisplayMemberPath; }
            set { searchList.DisplayMemberPath = value; }
        }

        /// <summary>Field which contains what properties will be searchable.</summary>
        private string[] properties;

        /// <summary>Each property, separated by a comma, that will be searchable in the object. Otherwise, every property is used.</summary>
        public string SearchProperties
        {
            get { return properties == null ? null : string.Join(",", properties); }
            set { properties = value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToArray(); }
        }

        /// <summary>Minimum number of characters that must be typed before the search starts.</summary>
        public int MinLength { get; set; }

        /// <summary>Maximum height of the results list.</summary>
        public double SearchListMaxHeight
        {
            get { return searchList.MaxHeight; }
            set { searchList.MaxHeight = value; }
        }

        /// <summary>Font size for the SearchBox.</summary>
        public double SearchBoxFontSize
        {
            get { return searchBox.FontSize; }
            set { searchBox.FontSize = value; }
        }

        /// <summary>Font size for the results list.</summary>
        public double SearchListFontSize
        {
            get { return searchList.FontSize; }
            set { searchList.FontSize = value; }
        }

        /// <summary>Mouse position over the results list.</summary>
        private Point mousePosition;
        #endregion

        #region EVENTS
        /// <summary>Identifies the SelectionCanged routed event.</summary>
        public static readonly RoutedEvent SelectionChangedEvent;

        /// <summary>Occurs when an item is selected or unselected on the SearchBox.</summary>
        public event RoutedPropertyChangedEventHandler<object> SelectionChanged
        {
            add { AddHandler(SelectionChangedEvent, value); }
            remove { RemoveHandler(SelectionChangedEvent, value); }
        }

        /// <summary>Handles the SelectionChanged logic.</summary>
        protected static void OnSelectionChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            RoutedPropertyChangedEventArgs<object> args = new RoutedPropertyChangedEventArgs<object>(e.OldValue, e.NewValue);
            args.RoutedEvent = SearchBox.SelectionChangedEvent;
            var searchBox = (SearchBox)sender;
            searchBox.RaiseEvent(args);
            searchBox.SetDisplayText();
        }
        #endregion

        #region CONSTRUCTORS
        /// <summary>Initializes a new instance of the SearchBox class.</summary>
        public SearchBox()
        {
            MinLength = 0;
            
            InitializeComponent();
            
            searchBox.AddHandler(TextBox.MouseLeftButtonUpEvent, new MouseButtonEventHandler(searchBox_MouseLeftButtonUpEvent), true); 
        }

        /// <summary>Registers routed events and dependency properties.</summary>
        static SearchBox()
        {
            SelectionChangedEvent = EventManager.RegisterRoutedEvent(
                "SelectionChanged", RoutingStrategy.Bubble,
                typeof(RoutedPropertyChangedEventHandler<object>), typeof(SearchBox));

            SelectedItemProperty = DependencyProperty.Register(
                "SelectedItem", typeof(object), typeof(SearchBox),
                new FrameworkPropertyMetadata(new PropertyChangedCallback(OnSelectionChanged)));

            ItemsSourceProperty = DependencyProperty.Register(
                "ItemsSource", typeof(IEnumerable), typeof(SearchBox));
        }
        #endregion

        #region METHODS
        /// <summary>Sets the SearchBox text to the SelectedItem value.</summary>
        private void SetDisplayText()
        {
            searchBox.Text = GetSelectedValue();
            searchList.ItemsSource = new[] { SelectedItem };
            searchBox.FontWeight = FontWeights.SemiBold;
            searchBox.FontStyle = FontStyles.Normal;
            searchBox.Foreground = Brushes.Black;
        }
        /// <summary>Executes a search and displays results.</summary>
        private void Search()
        {
            if (searchBox.Text.Length >= MinLength)
            {
                if (!searchPopup.IsOpen)
                {
                    searchPopup.IsOpen = true;
                }

                if (SearchFunction != null && searchBox.Text.Length > 0)
                {
                    searchList.ItemsSource = SearchFunction(searchBox.Text);
                }
                else
                {
                    if (searchBox.Text.Length == 0)
                    {
                        searchList.ItemsSource = ItemsSource;
                    }
                    else
                    {
                        searchList.ItemsSource = SearchFor(searchBox.Text);
                    }
                }

                if (searchList.Items.Count > 0)
                {
                    noResults.Visibility = Visibility.Collapsed;
                    searchList.Visibility = Visibility.Visible;
                    itemsCount.Visibility = Visibility.Visible;
                    itemsCount.Content = searchList.Items.Count.ToString() + " resultado(s)";
                }
                else
                {
                    searchList.Visibility = Visibility.Collapsed;
                    itemsCount.Visibility = Visibility.Collapsed;
                    noResults.Visibility = Visibility.Visible;
                }
            }
        }

        /// <summary>Searches a collection of items and returns a list filtered by the given string.</summary>
        /// <param name="str">String used as a filter.</param>
        /// <returns>A collection of objects filtered by the given string.</returns>
        private IEnumerable SearchFor(string str)
        {
            foreach (var obj in ItemsSource)
            {
                if (properties != null)
                {
                    foreach (var property in properties)
                    {
                        var value = GetPropertyValue(obj, property);

                        if (value != null && value.ToString().IndexOf(str, StringComparison.CurrentCultureIgnoreCase) > -1)
                        {
                            yield return obj;
                            break;
                        }
                    }
                }
                else
                {
                    if (obj != null && obj.ToString().IndexOf(str, StringComparison.CurrentCultureIgnoreCase) > -1)
                    {
                        yield return obj;
                    }
                }
            }
        }

        /// <summary>Gets the value of a specific property given an object.</summary>
        /// <param name="obj">Object to get the value from.</param>
        /// <param name="qualifiedProperty">The desired property.</param>
        /// <returns>The value obtained as an object.</returns>
        private object GetPropertyValue(object obj, string qualifiedProperty)
        {
            if (qualifiedProperty.Contains('.'))
            {
                foreach (var property in qualifiedProperty.Split('.'))
                {
                    obj = obj.GetType().GetProperty(property).GetValue(obj, null);

                    if (obj == null) return null;
                }
                return obj;
            }
            else
            {
                obj = obj.GetType().GetProperty(qualifiedProperty).GetValue(obj, null);
            }
            return obj;
        }

        /// <summary>Hides the results list.</summary>
        /// <param name="setValue">If true, displays de selected value on the SearchBox.</param>
        private void HideList(bool setValue)
        {
            if (setValue && searchList.SelectedItem != null)
            {
                SelectedItem = searchList.SelectedItem;
                searchBox.Text = GetSelectedValue();
                searchList.ItemsSource = new [] { SelectedItem };
                searchBox.FontWeight = FontWeights.SemiBold;
            }
            searchPopup.IsOpen = false;
            searchBox.FocusThatWorks();
            searchBox.CaretIndex = searchBox.Text.Length;
        }

        /// <summary>Gets the value of SelectedItem based on the DisplayMemberPath property or the ToString method if the former is null.</summary>
        /// <returns>The textual representation of SelectedItem.</returns>
        private string GetSelectedValue()
        {
            string selectedValue = null;
            if (SelectedItem != null)
            {
                if (DisplayMemberPath.HasValue())
                {
                    selectedValue = SelectedItem.GetType().GetProperty(DisplayMemberPath).GetValue(SelectedItem, null).ToString();
                }
                else
                {
                    selectedValue = SelectedItem.ToString();
                }
            }
            return selectedValue;
        }

        /// <summary>Scrolls the results list to the indicated item.</summary>
        /// <param name="index">Index of the item to scroll to.</param>
        public void ScrollToItem(int index)
        {
            if (index > -1 && index < searchList.Items.Count)
            {
                searchList.ScrollIntoView(searchList.SelectedItem);
            }
        }

        /// <summary>Resets the control to its initial state.</summary>
        public void Reset()
        {
            SelectedItem = null;
            searchList.ItemsSource = ItemsSource;
            searchBox.FontWeight = FontWeights.Normal;
            if (searchBox.IsFocused)
            {
                searchBox.FontStyle = FontStyles.Normal;
                searchBox.Foreground = Brushes.Black;
                searchBox.Text = string.Empty;
            }
            else
            {
                searchBox.FontStyle = FontStyles.Italic;
                searchBox.Foreground = Brushes.Gray;
                searchBox.Text = DefaultText;
            }
        }
        #endregion

        #region EVENT HANDLERS
        /// <summary>Displays the results list when MinLength is zero.</summary>
        private void searchBox_MouseLeftButtonUpEvent(object sender, MouseButtonEventArgs e)
        {
            if (MinLength == 0 && SelectedItem == null)
            {
                searchPopup.IsOpen = true;
            }
        }

        /// <summary>Hides the default text when the search box gets focus.</summary>
        private void searchBox_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
        {
            if (searchBox.Text == DefaultText)
            {
                searchBox.FontStyle = FontStyles.Normal;
                searchBox.Foreground = Brushes.Black;
                searchBox.Text = string.Empty;
            }
            if (MinLength == 0 && SelectedItem == null)
            {
                Search();
            }
        }

        /// <summary>Displays default text when the search box is empty and loses focus.</summary>
        private void searchBox_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
        {
            if (!searchBox.Text.Trim().HasValue() && !searchPopup.IsOpen)
            {
                searchBox.FontStyle = FontStyles.Italic;
                searchBox.Foreground = Brushes.Gray;
                searchBox.Text = DefaultText;
            }
        }

        /// <summary>Style plumbing when the search box is loaded.</summary>
        private void searchBox_Loaded(object sender, RoutedEventArgs e)
        {
            var scp = Util.FindVisualChild<ScrollContentPresenter>(searchBox);

            if (scp != null)
            {
                scp.HorizontalAlignment = HorizontalAlignment.Left;
                scp.Width = searchBox.ActualWidth - 28;
            }
        }

        /// <summary>Style plumbing when the list is loaded.</summary>
        private void searchList_Loaded(object sender, RoutedEventArgs e)
        {
            var border = Util.FindVisualChild<Border>(searchList);
            if (border != null)
            {
                border.Padding = new Thickness(0.0);
            }
        }

        /// <summary>Handles Up and Down key presses to give the effect of scrolling through the list when the key is held down.</summary>
        private void searchBox_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            if (ItemsSource == null) return;

            switch (e.Key)
            {
                case Key.Down:
                    if (searchList.SelectedIndex == -1)
                    {
                        if (!searchPopup.IsOpen && searchBox.Text.Length >= MinLength)
                        {
                            searchPopup.IsOpen = true;
                        }
                        searchList.SelectedIndex = 0;
                    }
                    else if (searchList.SelectedIndex + 1 < searchList.Items.Count)
                    {
                        searchList.SelectedIndex++;
                    }
                    else if (searchList.SelectedIndex + 1 == searchList.Items.Count)
                    {
                        searchList.SelectedIndex = -1;
                    }
                    ScrollToItem(searchList.SelectedIndex);
                    break;

                case Key.Up:
                    if (searchList.SelectedIndex > -1)
                    {
                        searchList.SelectedIndex--;
                    }
                    else if (searchList.SelectedIndex == -1)
                    {
                        if (!searchPopup.IsOpen && searchBox.Text.Length >= MinLength)
                        {
                            searchPopup.IsOpen = true;
                        }
                        searchList.SelectedIndex = searchList.Items.Count - 1;
                    }
                    ScrollToItem(searchList.SelectedIndex);
                    break;
            }
        }

        /// <summary>Handles key presses on the SearchBox and triggers the search function.</summary>
        private void searchBox_KeyUp(object sender, KeyEventArgs e)
        {
            if (ItemsSource == null && SearchFunction == null) return;

            switch (e.Key)
            {
                case Key.Enter:
                    if (searchPopup.IsOpen && searchList.SelectedItem != null)
                    {
                        HideList(true);
                    }
                    break;

                case Key.Escape:
                    searchPopup.IsOpen = false;
                    break;

                case Key.Delete:
                case Key.Back:
                    if (searchBox.Text.Length < MinLength)
                    {
                        searchList.ItemsSource = null;
                        searchPopup.IsOpen = false;
                        itemsCount.Content = "0 resultado(s)";
                    }
                    else
                    {
                        Search();
                    }
                    break;

                default:
                    Search();
                    break;
            }
        }

        /// <summary>Handles the behavior of text modifications on the SearchBox</summary>
        private void searchBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            if (SelectedItem != null)
            {
                var selectedValue = GetSelectedValue();
                if (selectedValue != null && !selectedValue.Equals(searchBox.Text, StringComparison.CurrentCultureIgnoreCase))
                {
                    SelectedItem = null;
                    searchBox.FontWeight = FontWeights.Normal;
                }
            }
        }

        /// <summary>Unselects any element when the results list is closed.</summary>
        private void searchPopup_Closed(object sender, EventArgs e)
        {
            searchList.SelectedIndex = -1;
        }

        /// <summary>Selects item on left click.</summary>
        private void searchList_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (searchList.SelectedItem != null)
            {
                HideList(true);
            }
        }

        /// <summary>Highlights items on the results list when the mouse is over.</summary>
        private void searchList_MouseMove(object sender, MouseEventArgs e)
        {
            var newMousePosition = Mouse.GetPosition(searchList);
            if (mousePosition != newMousePosition)
            {
                var item = sender as ListBoxItem;
                if (item != null)
                {
                    searchList.SelectedIndex = searchList.ItemContainerGenerator.IndexFromContainer(item);
                }
                mousePosition = newMousePosition;
            }
        }   
        #endregion
    }
}
